package ms.jasim.console.gui.component;

import java.awt.BorderLayout;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetContext;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.swing.ImageIcon;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.border.TitledBorder;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import ms.jasim.framework.PddlModel;
import ms.jasim.framework.PddlModel.Actor;
import ms.jasim.framework.PddlModel.Capacity;
import ms.jasim.framework.PddlModel.Decomposition;
import ms.jasim.framework.PddlModel.Goal;
import ms.spm.IAppContext;
import ms.utils.TreeNodeData;
import ms.utils.TreeNodeRenderer;

public class ME_RelationshipPanel extends JContextualPanel {

	static class RoleTreeModel extends DefaultTreeModel {

		private static final long serialVersionUID = 1L;
		private PddlModel model;

		public RoleTreeModel(TreeNode root) {
			super(root);
		}

		public void setModel(PddlModel model) {
			this.model = model;
			DefaultMutableTreeNode root = (DefaultMutableTreeNode) this.getRoot();

			for (PddlModel.Actor actor : model.Actors) {
				root.add(new RoleTreeNode(actor));
			}
			nodeChanged(root);
		}
	}

	static class GoalTreeModel extends DefaultTreeModel {

		private static final long serialVersionUID = 1L;
		private PddlModel model;

		public GoalTreeModel(TreeNode root) {
			super(root);
		}

		public void setModel(PddlModel model) {
			this.model = model;
			DefaultMutableTreeNode root = (DefaultMutableTreeNode) this.getRoot();

			for (PddlModel.Goal goal : model.Goals)
				root.add(new GoalTreeNode(goal));

			nodeChanged(root);
		}
	}

	static interface MyTreeNode {
		DataFlavor getSupportFlavor();

		void addChildren(Object data);
	}

	static interface LazyNode {
		boolean updateChildren();
	}

	static class RoleTreeNode extends DefaultMutableTreeNode implements LazyNode {

		private static final long serialVersionUID = 1L;
		private Actor actor;
		private boolean loaded;

		public RoleTreeNode(PddlModel.Actor actor) {
			super(new TreeNodeData(null, actor, ME_RelationshipPanel.ICON_ACTOR));
			this.actor = actor;

		}

		@Override
		public boolean updateChildren() {
			if (!loaded) {
				loaded = true;
				add(new ObjectListTreeNode<PddlModel.Capacity>(actor.Capacity, "Capacity", ME_RelationshipPanel.ICON_CAPACITY, ME_RelationshipPanel.ICON_GOAL) {

					private static final long serialVersionUID = 1L;

					@Override
					public void addChildren(Object arg0) {
						if (arg0 instanceof Goal) {
							if (!actor.containCapacity((Goal) arg0)) {
								Capacity capacity = actor.createCapacity();
								capacity.setGoal((Goal) arg0);
								addChildNode(capacity);
							}
						} else
							super.addChildren(arg0);
					}

					@Override
					protected MutableTreeNode createChildNode(Capacity arg0) {
						return new CapacityTreeNode(arg0);
					}
				});
				add(new ObjectListTreeNode<PddlModel.Goal>(actor.Requests, "Request", ME_RelationshipPanel.ICON_ACTOR_REQUEST, ME_RelationshipPanel.ICON_GOAL) {

					private static final long serialVersionUID = 1L;

					@Override
					public void addChildren(Object arg0) {
						if (arg0 instanceof Goal) {
							if (!actor.Requests.contains(arg0)) {
								actor.Requests.add((Goal) arg0);
								addChildNode((Goal) arg0);
							}
						} else
							super.addChildren(arg0);
					}

					@Override
					protected MutableTreeNode createChildNode(Goal arg0) {
						return new GoalTreeNode(arg0);
					}
				});
				return true;
			}
			return false;
		}

		@Override
		public boolean isLeaf() {
			return false;
		}
	}

	static class GoalTreeNode extends DefaultMutableTreeNode implements LazyNode {

		private static final long serialVersionUID = 1L;
		private Goal goal;
		private boolean loaded;

		public GoalTreeNode(PddlModel.Goal goal) {
			super(new TreeNodeData(null, goal, ME_RelationshipPanel.ICON_GOAL));
			this.goal = goal;
		}

		@Override
		public boolean updateChildren() {
			if (!loaded) {
				loaded = true;
				add(new ObjectListTreeNode<PddlModel.Decomposition>(goal.Decomposition, "Decomposition", ME_RelationshipPanel.ICON_DECOMPOSITION, ME_RelationshipPanel.ICON_GOAL) {

					private static final long serialVersionUID = 1L;

					@Override
					protected MutableTreeNode createChildNode(Decomposition arg0) {
						return new DecompositionTreeNode(arg0);
					}

					@SuppressWarnings("unchecked")
					@Override
					public void addChildren(Object arg0) {
						if (arg0 instanceof ArrayList) {
							Decomposition de = goal.createDecomposition();
							for (PddlModel.Goal g : (ArrayList<PddlModel.Goal>) arg0)
								de.SubGoals.add(g);
							addChildNode(de);
						} else
							super.addChildren(arg0);
					}
					
				});
				return true;
			}
			return false;
		}

		@Override
		public boolean isLeaf() {
			return false;
		}
	}

	static class CapacityTreeNode extends GoalTreeNode {

		private static final long serialVersionUID = 1L;

		public CapacityTreeNode(Capacity cap) {
			super(cap.getGoal());
			((TreeNodeData) getUserObject()).setData(cap);
		}
	}

	static class DecompositionTreeNode extends DefaultMutableTreeNode implements LazyNode {

		private static final long serialVersionUID = 1L;
		private Decomposition decomposition;
		private boolean loaded;

		public DecompositionTreeNode(PddlModel.Decomposition decomposition) {
			super(new TreeNodeData(null, decomposition, ICON_DECOMPOSITION));
			this.decomposition = decomposition;
		}

		@Override
		public boolean updateChildren() {
			if (!loaded) {
				loaded = true;
				for (PddlModel.Goal g : decomposition.SubGoals)
					add(new GoalTreeNode(g));
				return true;
			}
			return false;
		}
		
		@Override
		public boolean isLeaf() {
			return decomposition.SubGoals.size() == 0;
		}
	}

	static class ObjectListTreeNode<E> extends DefaultMutableTreeNode implements MyTreeNode, LazyNode {

		private static final long serialVersionUID = 1L;
		private ImageIcon subNodeIcon;
		private List<E> list;

		public ObjectListTreeNode(List<E> objectList, String text, ImageIcon icon, ImageIcon subNodeIcon) {
			setUserObject(new TreeNodeData(text, objectList, icon));

			this.subNodeIcon = subNodeIcon;
			this.list = objectList;

		}

		protected void addChildNode(E e) {
			add(createChildNode(e));
		}

		protected MutableTreeNode createChildNode(E e) {
			return new DefaultMutableTreeNode(new TreeNodeData(null, e, subNodeIcon));
		}

		@Override
		public boolean updateChildren() {
			ArrayList<Object> dataList = new ArrayList<Object>();
			boolean changed = false;
			
			// remove dangling node
			for (int i = this.getChildCount() - 1; i >= 0; i--) {
				Object data = ((TreeNodeData) ((DefaultMutableTreeNode) this.getChildAt(i)).getUserObject()).getData();
				changed |= !list.contains(data);
				if (!list.contains(data))
					remove(i);
				else
					dataList.add(data);
			}
			// create new node
			for (int i = 0; i < list.size(); i++) { 
				changed |= !dataList.contains(list.get(i));
				if (!dataList.contains(list.get(i)))
					addChildNode(list.get(i));
			}
			return changed;
		}

		@SuppressWarnings("unchecked")
		@Override
		public void addChildren(Object data) {
			if (data instanceof ArrayList)
				for (PddlModel.Goal g : (ArrayList<PddlModel.Goal>) data)
					addChildren(g);
		}

		@Override
		public DataFlavor getSupportFlavor() {
			return TreeTransferable.GOAL;
		}

		@Override
		public boolean isLeaf() {
			return list.size() == 0;
		}
	}

	static class TreeTransferable implements Transferable {

		public static final DataFlavor GOAL = new DataFlavor(PddlModel.Goal.class, "Goal");
		public static final DataFlavor ACTOR = new DataFlavor(PddlModel.Actor.class, "Actor");

		private HashMap<DataFlavor, ArrayList<Object>> data;

		public TreeTransferable(TreePath[] selections) {
			data = new HashMap<DataFlavor, ArrayList<Object>>();
			for (TreePath path : selections) {
				DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
				if (node instanceof RoleTreeNode)
					addData(ACTOR, ((TreeNodeData) node.getUserObject()).getData());
				else if (node instanceof GoalTreeNode)
					addData(GOAL, ((TreeNodeData) node.getUserObject()).getData());
			}
		}

		private void addData(DataFlavor flavor, Object obj) {
			ArrayList<Object> arr = data.get(flavor);
			if (arr == null)
				data.put(flavor, arr = new ArrayList<Object>());
			if (!arr.contains(obj))
				arr.add(obj);
		}

		@Override
		public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
			ArrayList<Object> arr = data.get(flavor);
			return arr;
		}

		@Override
		public DataFlavor[] getTransferDataFlavors() {
			return data.keySet().toArray(new DataFlavor[0]);
		}

		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return data.containsKey(flavor);
		}

	}

	class TreeNodeDrag implements DragSourceListener, DragGestureListener {

		private JTree sourceTree;
		private DragSource source;

		public TreeNodeDrag(JTree tree, int actions) {
			sourceTree = tree;
			source = new DragSource();
			source.createDefaultDragGestureRecognizer(sourceTree, actions, this);
		}

		@Override
		public void dragDropEnd(DragSourceDropEvent dsde) {
			// TODO Auto-generated method stub

		}

		@Override
		public void dragEnter(DragSourceDragEvent dsde) {
			// TODO Auto-generated method stub

		}

		@Override
		public void dragExit(DragSourceEvent dse) {
			// TODO Auto-generated method stub

		}

		@Override
		public void dragOver(DragSourceDragEvent dsde) {
			// TODO Auto-generated method stub

		}

		@Override
		public void dropActionChanged(DragSourceDragEvent dsde) {
			// TODO Auto-generated method stub

		}

		@Override
		public void dragGestureRecognized(DragGestureEvent dge) {
			TreePath path = sourceTree.getSelectionPath();
			if ((path == null) || (path.getPathCount() <= 1)) {
				return;
			}
			source.startDrag(dge, DragSource.DefaultCopyDrop, new TreeTransferable(sourceTree.getSelectionPaths()), this);

		}

	}

	class TreeDropTarget implements DropTargetListener {

		DropTarget target;
		JTree targetTree;

		public TreeDropTarget(JTree tree) {
			targetTree = tree;
			target = new DropTarget(targetTree, this);
		}

		/*
		 * Drop Event Handlers
		 */
		private TreeNode getNodeForEvent(DropTargetDragEvent dtde, boolean exactPosition) {
			Point p = dtde.getLocation();
			DropTargetContext dtc = dtde.getDropTargetContext();
			JTree tree = (JTree) dtc.getComponent();
			TreePath path = exactPosition ? tree.getPathForLocation(p.x, p.y) : tree.getClosestPathForLocation(p.x, p.y);
			return path != null ? (TreeNode) path.getLastPathComponent() : null;
		}

		private TreePath getTreePath(DropTargetDragEvent dtde) {
			Point p = dtde.getLocation();
			DropTargetContext dtc = dtde.getDropTargetContext();
			JTree tree = (JTree) dtc.getComponent();
			return tree.getPathForLocation(p.x, p.y);
		}

		public void dragEnter(DropTargetDragEvent dtde) {
			DefaultMutableTreeNode node = (DefaultMutableTreeNode) getNodeForEvent(dtde, false);

			if (node instanceof MyTreeNode) {
				dtde.acceptDrag(dtde.getDropAction());
			} else {
				dtde.rejectDrag();
			}

		}

		public void dragOver(DropTargetDragEvent dtde) {
			TreePath path = getTreePath(dtde);
			if (path != null) {
				DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
				if (node != null && node.getChildCount() > 0) {
					targetTree.expandPath(path);
					targetTree.setSelectionPath(path);
				}
				if (node instanceof MyTreeNode) {
					dtde.acceptDrag(dtde.getDropAction());
					targetTree.setSelectionPath(path);
				}
			} else
				dtde.rejectDrag();
		}

		public void dragExit(DropTargetEvent dte) {
		}

		public void dropActionChanged(DropTargetDragEvent dtde) {
		}

		public void drop(DropTargetDropEvent dtde) {
			Point pt = dtde.getLocation();
			DropTargetContext dtc = dtde.getDropTargetContext();
			JTree tree = (JTree) dtc.getComponent();
			TreePath parentpath = tree.getPathForLocation(pt.x, pt.y);
			if (parentpath == null) {
				dtde.rejectDrop();
				return;
			}

			DefaultMutableTreeNode parent = (DefaultMutableTreeNode) parentpath.getLastPathComponent();

			if (!(parent instanceof MyTreeNode)) {
				dtde.rejectDrop();
				return;
			}

			try {
				Transferable tr = dtde.getTransferable();
				MyTreeNode pTreeNode = (MyTreeNode) parent;

				if (tr.isDataFlavorSupported(pTreeNode.getSupportFlavor())) {
					dtde.acceptDrop(dtde.getDropAction());
					Object data = tr.getTransferData(pTreeNode.getSupportFlavor());
					pTreeNode.addChildren(data);
					((DefaultTreeModel) targetTree.getModel()).nodeStructureChanged(parent);
					dtde.dropComplete(true);
					return;
				}
				dtde.rejectDrop();
			} catch (Exception e) {
				e.printStackTrace();
				dtde.rejectDrop();
			}
		}
	}

	class TreeWillExpand implements TreeWillExpandListener {

		@Override
		public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
			// TODO Auto-generated method stub

		}

		@Override
		public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
			// TODO Auto-generated method stub
			Object node = event.getPath().getLastPathComponent();
			if (node instanceof LazyNode)
				if (((LazyNode) node).updateChildren())
					((DefaultTreeModel) ((JTree) event.getSource()).getModel()).nodeStructureChanged((TreeNode) node);
		}

	}

	public static final ImageIcon ICON_GROUP = new ImageIcon("resources/group_24x24.png");
	public static final ImageIcon ICON_ACTOR = new ImageIcon("resources/user_16x16.png");
	public static final ImageIcon ICON_CAPACITY = new ImageIcon("resources/capacity_16x16.png");
	public static final ImageIcon ICON_GOAL = new ImageIcon("resources/goal_16x16.png");
	public static final ImageIcon ICON_ACTOR_REQUEST = new ImageIcon("resources/request_16x16.png");
	public static final ImageIcon ICON_DECOMPOSITION = new ImageIcon("resources/relationship_16x16.png");

	private static final long serialVersionUID = 1L;
	private PddlModel model;
	private RoleTreeModel roleTreeModel;
	private GoalTreeModel goalTreeModel;

	public ME_RelationshipPanel() {
		setLayout(new BorderLayout());

		TreeNodeRenderer defaultTreeNodeRenderer = new TreeNodeRenderer();
		TreeWillExpand defaultExpander = new TreeWillExpand();
		
		// create the RoleTree
		JTree roleTree = new JTree(roleTreeModel = new RoleTreeModel(new DefaultMutableTreeNode(new TreeNodeData("Roles", null, ICON_GROUP))));
		roleTree.setCellRenderer(defaultTreeNodeRenderer);
		roleTree.getSelectionModel().setSelectionMode(TreeSelectionModel.CONTIGUOUS_TREE_SELECTION);
		roleTree.addTreeWillExpandListener(defaultExpander);

		// create the RoleTreePanel
		JScrollPane roleTreeScrollPane = new JScrollPane(roleTree);
		roleTreeScrollPane.setBorder(new TitledBorder("Available Roles"));

		// create the GoalTree
		JTree goalTree = new JTree(goalTreeModel = new GoalTreeModel(new DefaultMutableTreeNode(new TreeNodeData("Goals", null, ICON_GOAL))));
		goalTree.setCellRenderer(defaultTreeNodeRenderer);
		goalTree.addTreeWillExpandListener(defaultExpander);
		
		// create the GoalTreePanel
		JScrollPane goalTreeScrollPane = new JScrollPane(goalTree);
		goalTreeScrollPane.setBorder(new TitledBorder("Goals"));

		// create the splitter
		JSplitPane splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, roleTreeScrollPane, goalTreeScrollPane);
		splitter.setResizeWeight(0.5);

		add(splitter, BorderLayout.CENTER);

		new TreeNodeDrag(goalTree, DnDConstants.ACTION_LINK);
		new TreeDropTarget(roleTree);
		new TreeDropTarget(goalTree);
	}

	@Override
	public void setContext(IAppContext context) {
		super.setContext(context);
		model = context.getService(PddlModel.class);

		roleTreeModel.setModel(model);
		goalTreeModel.setModel(model);
	}
}
